<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SpringBoot项目中使用SpringSecurity和JWT做权限认证 | 程序员小航</title><meta name="keywords" content="JWT,SpringSecurity"><meta name="author" content="liuzhihang"><meta name="copyright" content="liuzhihang"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="#ffffff"><meta name="description" content="前端和后端使用Json字符串进行交互. 使用JWT生成token, SpringSecurity做权限认证. 在请求某些接口时, 自动做token权限认证和URL拦截请求等操作.">
<meta property="og:type" content="article">
<meta property="og:title" content="SpringBoot项目中使用SpringSecurity和JWT做权限认证">
<meta property="og:url" content="https://liuzhihang.com/2019/07/22/springsecurity-jwt-springboot-project.html">
<meta property="og:site_name" content="程序员小航">
<meta property="og:description" content="前端和后端使用Json字符串进行交互. 使用JWT生成token, SpringSecurity做权限认证. 在请求某些接口时, 自动做token权限认证和URL拦截请求等操作.">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/cyber-security.jpg">
<meta property="article:published_time" content="2019-07-22T05:36:22.000Z">
<meta property="article:modified_time" content="2020-12-27T09:23:14.248Z">
<meta property="article:author" content="liuzhihang">
<meta property="article:tag" content="JWT">
<meta property="article:tag" content="SpringSecurity">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/cyber-security.jpg"><link rel="shortcut icon" href="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/favicon.ico"><link rel="canonical" href="https://liuzhihang.com/2019/07/22/springsecurity-jwt-springboot-project"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="//www.google-analytics.com" crossorigin=""/><link rel="preconnect" href="//pingjs.qq.com"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><meta name="google_site_verification" content="GZshAnnrC-4eCrl5h-e_5Rdk4lOUhRK7ShULoRi2q0E"/><link rel="stylesheet" href="/css/index.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css" media="print" onload="this.media='all'"><script async="async" src="https://www.googletagmanager.com/gtag/js?id=UA-126394362-1"></script><script>window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-126394362-1');
</script><script>var _mtac = {};
(function() {
    var mta = document.createElement("script");
    mta.src = "//pingjs.qq.com/h5/stats.js?v2.0.4";
    mta.setAttribute("name", "MTAH5");
    mta.setAttribute("sid", "66540676");
    var s = document.getElementsByTagName("script")[0];
    s.parentNode.insertBefore(mta, s);
})();
</script><script>const GLOBAL_CONFIG = { 
  root: '/',
  algolia: undefined,
  localSearch: {"path":"search.xml","languages":{"hits_empty":"找不到您查询的内容：${query}"}},
  translate: {"defaultEncoding":2,"translateDelay":0,"msgToTraditionalChinese":"繁","msgToSimplifiedChinese":"簡"},
  noticeOutdate: {"limitDay":730,"position":"top","messagePrev":"It has been","messageNext":"days since the last update, the content of the article may be outdated."},
  highlight: undefined,
  copy: {
    success: '复制成功',
    error: '复制错误',
    noSupport: '浏览器不支持'
  },
  relativeDate: {
    homepage: false,
    post: false
  },
  runtime: '',
  date_suffix: {
    just: '刚刚',
    min: '分钟前',
    hour: '小时前',
    day: '天前',
    month: '个月前'
  },
  copyright: undefined,
  lightbox: 'fancybox',
  Snackbar: undefined,
  source: {
    jQuery: 'https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js',
    justifiedGallery: {
      js: 'https://cdn.jsdelivr.net/npm/justifiedGallery/dist/js/jquery.justifiedGallery.min.js',
      css: 'https://cdn.jsdelivr.net/npm/justifiedGallery/dist/css/justifiedGallery.min.css'
    },
    fancybox: {
      js: 'https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@latest/dist/jquery.fancybox.min.js',
      css: 'https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@latest/dist/jquery.fancybox.min.css'
    }
  },
  isPhotoFigcaption: false,
  islazyload: true,
  isanchor: false
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
  title: 'SpringBoot项目中使用SpringSecurity和JWT做权限认证',
  isPost: true,
  isHome: false,
  isHighlightShrink: false,
  isToc: true,
  postUpdate: '2020-12-27 17:23:14'
}</script><noscript><style type="text/css">
  #nav {
    opacity: 1
  }
  .justified-gallery img {
    opacity: 1
  }

  #recent-posts time,
  #post-meta time {
    display: inline !important
  }
</style></noscript><script>(win=>{
    win.saveToLocal = {
      set: function setWithExpiry(key, value, ttl) {
        if (ttl === 0) return
        const now = new Date()
        const expiryDay = ttl * 86400000
        const item = {
          value: value,
          expiry: now.getTime() + expiryDay,
        }
        localStorage.setItem(key, JSON.stringify(item))
      },

      get: function getWithExpiry(key) {
        const itemStr = localStorage.getItem(key)

        if (!itemStr) {
          return undefined
        }
        const item = JSON.parse(itemStr)
        const now = new Date()

        if (now.getTime() > item.expiry) {
          localStorage.removeItem(key)
          return undefined
        }
        return item.value
      }
    }
  
    win.getScript = url => new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = url
      script.async = true
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    })
  
      win.activateDarkMode = function () {
        document.documentElement.setAttribute('data-theme', 'dark')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
        }
      }
      win.activateLightMode = function () {
        document.documentElement.setAttribute('data-theme', 'light')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#ffffff')
        }
      }
      const t = saveToLocal.get('theme')
    
          if (t === 'dark') activateDarkMode()
          else if (t === 'light') activateLightMode()
        
      const asideStatus = saveToLocal.get('aside-status')
      if (asideStatus !== undefined) {
        if (asideStatus === 'hide') {
          document.documentElement.classList.add('hide-aside')
        } else {
          document.documentElement.classList.remove('hide-aside')
        }
      }
    
    const detectApple = () => {
      if (GLOBAL_CONFIG_SITE.isHome && /iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
        document.documentElement.classList.add('apple')
      }
    }
    detectApple()
    })(window)</script><link rel="stylesheet" href="//at.alicdn.com/t/font_1608878_xsf93f0c1a.css"><meta name="generator" content="Hexo 5.2.0"><link rel="alternate" href="/atom.xml" title="程序员小航" type="application/atom+xml">
</head><body><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/404/avatar.jpg" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="site-data"><div class="data-item is-center"><div class="data-item-link"><a href="/archives/"><div class="headline">文章</div><div class="length-num">145</div></a></div></div><div class="data-item is-center"><div class="data-item-link"><a href="/tags/"><div class="headline">标签</div><div class="length-num">55</div></a></div></div><div class="data-item is-center"><div class="data-item-link"><a href="/categories/"><div class="headline">分类</div><div class="length-num">31</div></a></div></div></div><hr/><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="javascript:void(0);"><i class="fa-fw fas fa-layer-group"></i><span> 导航</span><i class="fas fa-chevron-down expand hide"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 归档</span></a></li><li><a class="site-page child" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></li><li><a class="site-page child" href="/categories/"><i class="fa-fw fas fa-bookmark"></i><span> 分类</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> 推荐</span><i class="fas fa-chevron-down expand hide"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/books/"><i class="fa-fw fas fa-book"></i><span> 书单</span></a></li><li><a class="site-page child" href="/movies/"><i class="fa-fw fas fa-film"></i><span> 电影</span></a></li><li><a class="site-page child" href="/games/"><i class="fa-fw fas fa-gamepad"></i><span> 游戏</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/contact/"><i class="fa-fw fas fa-comments"></i><span> 留言板</span></a></div><div class="menus_item"><a class="site-page" href="/link/"><i class="fa-fw fas fa-address-book"></i><span> 友链</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-user-circle"></i><span> 关于</span></a></div><div class="menus_item"><a class="site-page" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> 其他</span><i class="fas fa-chevron-down expand hide"></i></a><ul class="menus_item_child"><li><a class="site-page child" target="_blank" rel="noopener" href="https://juejin.im/user/1987506650493117"><i class="fa-fw iconfont iconjuejin-1"></i><span> 掘金</span></a></li><li><a class="site-page child" target="_blank" rel="noopener" href="https://www.zhihu.com/people/liuzhihang"><i class="fa-fw iconfont iconzhihu1"></i><span> 知乎</span></a></li><li><a class="site-page child" target="_blank" rel="noopener" href="https://www.infoq.cn/profile/00B8AE08DA916E/publish"><i class="fa-fw iconfont iconweibiaoti-1"></i><span> InfoQ</span></a></li><li><a class="site-page child" target="_blank" rel="noopener" href="https://blog.csdn.net/qq_36535538"><i class="fa-fw iconfont iconcsdn"></i><span> CSDN</span></a></li><li><a class="site-page child" target="_blank" rel="noopener" href="https://leetcode-cn.com/u/liuzhihang/"><i class="fa-fw iconfont iconleetcode"></i><span> LeetCode</span></a></li></ul></div></div></div></div><div class="post" id="body-wrap"><header class="post-bg" id="page-header" style="background-image: url('https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/cyber-security.jpg')"><nav id="nav"><span id="blog_name"><a id="site-name" href="/">程序员小航</a></span><div id="menus"><div id="search-button"><a class="site-page social-icon search"><i class="fas fa-search fa-fw"></i><span> 搜索</span></a></div><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="javascript:void(0);"><i class="fa-fw fas fa-layer-group"></i><span> 导航</span><i class="fas fa-chevron-down expand hide"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 归档</span></a></li><li><a class="site-page child" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></li><li><a class="site-page child" href="/categories/"><i class="fa-fw fas fa-bookmark"></i><span> 分类</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> 推荐</span><i class="fas fa-chevron-down expand hide"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/books/"><i class="fa-fw fas fa-book"></i><span> 书单</span></a></li><li><a class="site-page child" href="/movies/"><i class="fa-fw fas fa-film"></i><span> 电影</span></a></li><li><a class="site-page child" href="/games/"><i class="fa-fw fas fa-gamepad"></i><span> 游戏</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/contact/"><i class="fa-fw fas fa-comments"></i><span> 留言板</span></a></div><div class="menus_item"><a class="site-page" href="/link/"><i class="fa-fw fas fa-address-book"></i><span> 友链</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-user-circle"></i><span> 关于</span></a></div><div class="menus_item"><a class="site-page" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> 其他</span><i class="fas fa-chevron-down expand hide"></i></a><ul class="menus_item_child"><li><a class="site-page child" target="_blank" rel="noopener" href="https://juejin.im/user/1987506650493117"><i class="fa-fw iconfont iconjuejin-1"></i><span> 掘金</span></a></li><li><a class="site-page child" target="_blank" rel="noopener" href="https://www.zhihu.com/people/liuzhihang"><i class="fa-fw iconfont iconzhihu1"></i><span> 知乎</span></a></li><li><a class="site-page child" target="_blank" rel="noopener" href="https://www.infoq.cn/profile/00B8AE08DA916E/publish"><i class="fa-fw iconfont iconweibiaoti-1"></i><span> InfoQ</span></a></li><li><a class="site-page child" target="_blank" rel="noopener" href="https://blog.csdn.net/qq_36535538"><i class="fa-fw iconfont iconcsdn"></i><span> CSDN</span></a></li><li><a class="site-page child" target="_blank" rel="noopener" href="https://leetcode-cn.com/u/liuzhihang/"><i class="fa-fw iconfont iconleetcode"></i><span> LeetCode</span></a></li></ul></div></div><div id="toggle-menu"><a class="site-page"><i class="fas fa-bars fa-fw"></i></a></div></div></nav><div id="post-info"><h1 class="post-title">SpringBoot项目中使用SpringSecurity和JWT做权限认证</h1><div id="post-meta"><div class="meta-firstline"><span class="post-meta-date"><i class="far fa-calendar-alt fa-fw post-meta-icon"></i><span class="post-meta-label">发表于</span><time class="post-meta-date-created" datetime="2019-07-22T05:36:22.000Z" title="发表于 2019-07-22 13:36:22">2019-07-22</time><span class="post-meta-separator">|</span><i class="fas fa-history fa-fw post-meta-icon"></i><span class="post-meta-label">更新于</span><time class="post-meta-date-updated" datetime="2020-12-27T09:23:14.248Z" title="更新于 2020-12-27 17:23:14">2020-12-27</time></span><span class="post-meta-categories"><span class="post-meta-separator">|</span><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/Spring/">Spring</a></span></div><div class="meta-secondline"><span class="post-meta-separator">|</span><span class="post-meta-wordcount"><i class="far fa-file-word fa-fw post-meta-icon"></i><span class="post-meta-label">字数总计:</span><span class="word-count">3.1k</span><span class="post-meta-separator">|</span><i class="far fa-clock fa-fw post-meta-icon"></i><span class="post-meta-label">阅读时长:</span><span>15分钟</span></span><span class="post-meta-separator">|</span><span class="post-meta-pv-cv" id="" data-flag-title="SpringBoot项目中使用SpringSecurity和JWT做权限认证"><i class="far fa-eye fa-fw post-meta-icon"></i><span class="post-meta-label">阅读量:</span><span id="busuanzi_value_page_pv"></span></span><span class="post-meta-separator">|</span><span class="post-meta-commentcount"><i class="far fa-comments fa-fw post-meta-icon"></i><span class="post-meta-label">评论数:</span><a href="/2019/07/22/springsecurity-jwt-springboot-project.html#post-comment"><span class="gitalk-comment-count"></span></a></span></div></div></div></header><main class="layout" id="content-inner"><div id="post"><article class="post-content" id="article-container"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><blockquote>
<p>前段时间做了一个项目, 因为涉及到权限认证, 所以分别调研了 SpringSecurity 和 Apache Shiro. 最后选择使用了 SpringSecurity + JWT做权限认证,  现在项目已经结束, 总相关笔记.<br>项目下载地址 <a target="_blank" rel="noopener" href="https://github.com/liuzhihang/jwt-demo">jwt-demo</a></p>
</blockquote>
<ol>
<li>使用JWT生成token</li>
<li>token存储在数据库中</li>
<li>使用 application/json 登录</li>
<li>使用手机号进行登录</li>
<li>URI动态拦截</li>
</ol>
<h2 id="配置过程"><a href="#配置过程" class="headerlink" title="配置过程"></a>配置过程</h2><h3 id="添加依赖"><a href="#添加依赖" class="headerlink" title="添加依赖"></a>添加依赖</h3><ol>
<li>分别添加 SpringSecurity JWT 和 fastjson 依赖</li>
</ol>
<pre><code class="xml">    &lt;dependency&gt;
           &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
           &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
       &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
        &lt;artifactId&gt;jjwt&lt;/artifactId&gt;
        &lt;version&gt;0.9.0&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;!--json--&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
        &lt;artifactId&gt;fastjson&lt;/artifactId&gt;
        &lt;version&gt;1.2.60&lt;/version&gt;
    &lt;/dependency&gt;</code></pre>
<h3 id="基础准备对象"><a href="#基础准备对象" class="headerlink" title="基础准备对象"></a>基础准备对象</h3><ul>
<li>主要是在<strong>用户登录成功handle</strong>时使用JWT生成Token返回给客户端.</li>
</ul>
<h4 id="基础使用dto"><a href="#基础使用dto" class="headerlink" title="基础使用dto"></a>基础使用dto</h4><p>请求返回基类</p>
<pre><code class="java">@Data
public class BaseReqDto implements Serializable &#123;

    private String version;

&#125;

@Data
public class BaseRespDto implements Serializable &#123;

    private String resultCode;

    private String resultMsg;

    private String resultTime;

&#125;</code></pre>
<p>登录请求返回对象</p>
<pre><code class="java">@Data
public class LoginReqDto &#123;

    private String username;

    private String token;

&#125;

@Data
public class LoginRespDto extends BaseRespDto &#123;

    private String token;

&#125;</code></pre>
<h4 id="用于验证的用户"><a href="#用于验证的用户" class="headerlink" title="用于验证的用户"></a>用于验证的用户</h4><pre><code class="java">package com.liuzhihang.demo.bean;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;

/**
 * 用户信息校验验证码
 *
 * @author liuzhihang
 */
public class UserDetailsImpl implements UserDetails, Serializable &#123;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 权限集合
     */
    private Collection&lt;? extends GrantedAuthority&gt; authorities;

    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() &#123;
        return this.authorities;
    &#125;

    public void setAuthorities(Collection&lt;? extends GrantedAuthority&gt; authorities) &#123;
        this.authorities = authorities;
    &#125;

    @Override
    public String getPassword() &#123;
        return this.password;
    &#125;

    @Override
    public String getUsername() &#123;
        return this.username;
    &#125;

    public void setUsername(String username) &#123;
        this.username = username;
    &#125;

    public void setPassword(String password) &#123;
        this.password = password;
    &#125;

    @Override
    public boolean isAccountNonExpired() &#123;
        return true;
    &#125;

    @Override
    public boolean isAccountNonLocked() &#123;
        return true;
    &#125;

    @Override
    public boolean isCredentialsNonExpired() &#123;
        return true;
    &#125;

    @Override
    public boolean isEnabled() &#123;
        return true;
    &#125;
&#125;</code></pre>
<h4 id="用户未登录handle"><a href="#用户未登录handle" class="headerlink" title="用户未登录handle"></a>用户未登录handle</h4><pre><code class="java">
/**
 * 用户登录认证, 未登录返回信息
 *
 * @author liuzhihang
 * @date 2019-06-04 13:52
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint &#123;

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(&quot;yyyyMMddHHmmss&quot;);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException &#123;

        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        LoginRespDto respDto = new LoginRespDto();
        respDto.setResultCode(&quot;0001&quot;);
        respDto.setResultMsg(&quot;用户未登录&quot;);
        respDto.setResultTime(LocalDateTime.now().format(FORMATTER));

        response.getWriter().write(JSON.toJSONString(respDto));
    &#125;
&#125;
</code></pre>
<h4 id="用户登录验证失败handle"><a href="#用户登录验证失败handle" class="headerlink" title="用户登录验证失败handle"></a>用户登录验证失败handle</h4><pre><code class="java">/**
 * 用户登录认证失败返回的信息
 *
 * @author liuzhihang
 * @date 2019-06-04 13:57
 */
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler &#123;

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(&quot;yyyyMMddHHmmss&quot;);

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException &#123;

        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        LoginRespDto respDto = new LoginRespDto();
        respDto.setResultCode(&quot;0001&quot;);
        respDto.setResultMsg(&quot;用户登录认证失败&quot;);
        respDto.setResultTime(LocalDateTime.now().format(FORMATTER));

        response.getWriter().write(JSON.toJSONString(respDto));
    &#125;
&#125;</code></pre>
<h4 id="用户无权访问handle"><a href="#用户无权访问handle" class="headerlink" title="用户无权访问handle"></a>用户无权访问handle</h4><pre><code class="java">/**
 * 当用户访问无权限页面时, 返回信息
 *
 * @author liuzhihang
 * @date 2019-06-04 14:03
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler &#123;

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(&quot;yyyyMMddHHmmss&quot;);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException &#123;

        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        LoginRespDto respDto = new LoginRespDto();
        respDto.setResultCode(&quot;0002&quot;);
        respDto.setResultMsg(&quot;用户无权访问&quot;);
        respDto.setResultTime(LocalDateTime.now().format(FORMATTER));

        response.getWriter().write(JSON.toJSONString(respDto));

    &#125;
&#125;
</code></pre>
<h4 id="用户登录成功handle"><a href="#用户登录成功handle" class="headerlink" title="用户登录成功handle"></a>用户登录成功handle</h4><pre><code class="java">
/**
 * 用户登录成功之后的返回信息
 *
 * @author liuzhihang
 * @date 2019-06-04 14:20
 */
@Slf4j
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler &#123;

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(&quot;yyyyMMddHHmmss&quot;);

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException &#123;

        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();

        String jwtToken = jwtTokenUtil.generateToken(userDetails);

        // 把生成的token更新到数据库中
        // 更新DB操作 ...

        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        LoginRespDto respDto = new LoginRespDto();
        respDto.setToken(jwtToken);
        respDto.setResultCode(&quot;0000&quot;);
        respDto.setResultMsg(&quot;登录成功&quot;);
        respDto.setResultTime(LocalDateTime.now().format(FORMATTER));

        response.getWriter().write(JSON.toJSONString(respDto));

    &#125;
&#125;</code></pre>
<h2 id="JwtTokenUtil"><a href="#JwtTokenUtil" class="headerlink" title="JwtTokenUtil"></a>JwtTokenUtil</h2><p>主要用来生成token和通过token解析对象等操作.</p>
<pre><code class="java">package com.liuzhihang.demo.utils;

import com.liuzhihang.demo.bean.UserDetailsImpl;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.Date;

/**
 * 使用 java-jwt jwt类库
 *
 * @author liuzhihang
 * @date 2019-06-05 09:22
 */
@Component
public class JwtTokenUtil &#123;

    private static final SignatureAlgorithm SIGN_TYPE = SignatureAlgorithm.HS256;

    public static final String SECRET = &quot;jwt-secret&quot;;

    /**
     * JWT超时时间
     */
    public static final long EXPIRED_TIME = 7 * 24 * 60 * 60 * 1000L;

    /**
     * claims 为自定义的私有声明, 要放在前面
     * &lt;p&gt;
     * 生成token
     */
    public String generateToken(UserDetails userDetails) &#123;

        long instantNow = Instant.now().toEpochMilli();

        Claims claims = Jwts.claims();
        claims.put(Claims.SUBJECT, userDetails.getUsername());

        return Jwts.builder().setClaims(claims).setIssuedAt(new Date(instantNow))
                .setExpiration(new Date(instantNow + EXPIRED_TIME))
                .signWith(SIGN_TYPE, SECRET).compact();
    &#125;

    /**
     * claims 为自定义的私有声明, 要放在前面
     * &lt;p&gt;
     * 生成token
     */
    public String generateToken(String userName) &#123;

        long instantNow = Instant.now().toEpochMilli();

        Claims claims = Jwts.claims();
        claims.put(Claims.SUBJECT, userName);

        return Jwts.builder().setClaims(claims).setIssuedAt(new Date(instantNow))
                .setExpiration(new Date(instantNow + EXPIRED_TIME))
                .signWith(SIGN_TYPE, SECRET).compact();
    &#125;

    /**
     * 将token解析, 映射为 UserDetails
     *
     * @param jwtToken
     * @return
     */
    public UserDetails getUserDetailsFromToken(String jwtToken) &#123;

        Claims claimsFromToken = getClaimsFromToken(jwtToken);

        String userName = claimsFromToken.get(Claims.SUBJECT, String.class);

        UserDetailsImpl userDetails = new UserDetailsImpl();
        userDetails.setUsername(userName);

        return userDetails;
    &#125;

    /**
     * 验证token
     */
    public Boolean validateToken(String token, UserDetails userDetails) &#123;
        UserDetailsImpl user = (UserDetailsImpl) userDetails;
        String username = getPhoneNoFromToken(token);

        return (username.equals(user.getUsername()) &amp;&amp; !isTokenExpired(token));
    &#125;

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) &#123;
        String refreshedToken;
        try &#123;
            Claims claims = getClaimsFromToken(token);

            long instantNow = Instant.now().toEpochMilli();

            refreshedToken = Jwts.builder().setClaims(claims).setIssuedAt(new Date(instantNow))
                    .setExpiration(new Date(instantNow + EXPIRED_TIME))
                    .signWith(SIGN_TYPE, SECRET).compact();
        &#125; catch (Exception e) &#123;
            refreshedToken = null;
        &#125;
        return refreshedToken;
    &#125;

    /**
     * 获取token是否过期
     */
    public Boolean isTokenExpired(String token) &#123;
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    &#125;

    /**
     * 根据token获取username
     */
    public String getPhoneNoFromToken(String token) &#123;
        return getClaimsFromToken(token).getSubject();
    &#125;

    /**
     * 获取token的过期时间
     */
    public Date getExpirationDateFromToken(String token) &#123;
        return getClaimsFromToken(token).getExpiration();
    &#125;

    /**
     * 解析JWT
     */
    private Claims getClaimsFromToken(String token) &#123;
        return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
    &#125;

&#125;
</code></pre>
<h2 id="WebSecurityConfig-核心配置"><a href="#WebSecurityConfig-核心配置" class="headerlink" title="WebSecurityConfig 核心配置"></a>WebSecurityConfig 核心配置</h2><pre><code class="java">package com.liuzhihang.demo.config;

import com.liuzhihang.demo.filter.CustomizeAuthenticationFilter;
import com.liuzhihang.demo.filter.JwtPerTokenFilter;
import com.liuzhihang.demo.service.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

/**
 * @author liuzhihang
 * @date 2019-06-03 14:25
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter &#123;

    @Autowired
    private UserDetailServiceImpl userDetailServiceImpl;

    @Resource
    private JwtPerTokenFilter jwtPerTokenFilter;

    @Resource(name = &quot;authenticationEntryPointImpl&quot;)
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Resource(name = &quot;authenticationSuccessHandlerImpl&quot;)
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Resource(name = &quot;authenticationFailureHandlerImpl&quot;)
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Resource(name = &quot;accessDeniedHandlerImpl&quot;)
    private AccessDeniedHandler accessDeniedHandler;

    /**
     * 创建用于认证授权的用户
     *
     * @param auth
     * @throws Exception
     */
    @Autowired
    public void configureUserInfo(AuthenticationManagerBuilder auth) throws Exception &#123;

        // 放入自己的认证授权用户, 内部逻辑需要自己实现
        // UserDetailServiceImpl implements UserDetailsService
        auth.userDetailsService(userDetailServiceImpl);

    &#125;

    @Override
    protected void configure(HttpSecurity http) throws Exception &#123;
        http
                // 使用JWT, 关闭session
                .csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and().httpBasic().authenticationEntryPoint(authenticationEntryPoint)

                // 登录的权限, 成功返回信息, 失败返回信息
                .and().formLogin().permitAll()

                .loginProcessingUrl(&quot;/login&quot;)

                // 配置url 权限 antMatchers: 匹配url 权限
                .and().authorizeRequests()
                .antMatchers(&quot;/login&quot;, &quot;/getVersion&quot;)
                .permitAll()
                // 其他需要登录才能访问
                .anyRequest().access(&quot;@dynamicAuthorityService.hasPermission(request,authentication)&quot;)

                // 访问无权限 location 时
                .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)

                // 自定义过滤
                .and().addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jwtPerTokenFilter, UsernamePasswordAuthenticationFilter.class)

                .headers().cacheControl();

    &#125;


    /**
     * 密码加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder() &#123;
        /**
         * BCryptPasswordEncoder：相同的密码明文每次生成的密文都不同，安全性更高
         */
        return new BCryptPasswordEncoder();
    &#125;

    @Bean
    CustomizeAuthenticationFilter customAuthenticationFilter() throws Exception &#123;
        CustomizeAuthenticationFilter filter = new CustomizeAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        filter.setAuthenticationFailureHandler(authenticationFailureHandler);
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    &#125;

&#125;
</code></pre>
<h2 id="登录校验过程"><a href="#登录校验过程" class="headerlink" title="登录校验过程"></a>登录校验过程</h2><div class="mermaid">graph TD;
    A(请求登录) --&gt; B(CustomizeAuthenticationFilter#attemptAuthentication 解析请求的json);
    B --&gt; C(UserDetailServiceImpl#loadUserByUsername 验证用户名密码);
    C --&gt; D(AuthenticationSuccessHandlerImpl#onAuthenticationSuccess 构建返回参数 包括token);
    D --&gt; E(返回结果)</div>

<h3 id="自定义拦截器解析-json-报文"><a href="#自定义拦截器解析-json-报文" class="headerlink" title="自定义拦截器解析 json 报文"></a>自定义拦截器解析 json 报文</h3><p>前端请求登录报文类型为 application/json 需要后端增加拦截器, 对登录请求报文进行解析</p>
<pre><code class="java">package com.liuzhihang.demo.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

/**
 *
 * 自定义拦截器, 重写UsernamePasswordAuthenticationFilter 从而可以处理 application/json 中的json请求报文
 *
 * @author liuzhihang
 * @date 2019-06-12 19:04
 */
@Slf4j
public class CustomizeAuthenticationFilter extends UsernamePasswordAuthenticationFilter &#123;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException &#123;

        // attempt Authentication when Content-Type is json
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)
            || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) &#123;
            try &#123;
                BufferedReader br = request.getReader();
                String str;
                StringBuilder jsonStr = new StringBuilder();
                while ((str = br.readLine()) != null) &#123;
                    jsonStr.append(str);
                &#125;

                log.info(&quot;本次登录请求参数:&#123;&#125;&quot;, jsonStr);

                JSONObject jsonObject = JSON.parseObject(jsonStr.toString());

                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    jsonObject.getString(&quot;username&quot;), jsonObject.getString(&quot;password&quot;));
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            &#125; catch (IOException e) &#123;
                log.info(&quot;用户登录, 请求参数 不正确&quot;);
                throw new AuthenticationServiceException(&quot;获取报文请求参数失败&quot;);
            &#125; catch (JSONException e) &#123;
                log.info(&quot;用户登录, 请求报文格式 不正确&quot;);
                throw new AuthenticationServiceException(&quot;请求报文, 转换Json失败&quot;);
            &#125;
        &#125; else &#123;
            log.error(&quot;用户登录, contentType 不正确&quot;);
            throw new AuthenticationServiceException(
                &quot;请求 contentType 不正确, 请使用 application/json;charset=UTF-8 或者 application/json;&quot;);
        &#125;

    &#125;

&#125;
</code></pre>
<h3 id="用户认证模块"><a href="#用户认证模块" class="headerlink" title="用户认证模块"></a>用户认证模块</h3><ul>
<li>根据获取到的username从数据库中查询到密码, 将用户名密码赋值给UserDetails对象, 返回其他的框架会进行校验</li>
<li>这边使用中是使用的手机号+验证码登录, 所以 上面json解析的也是 phoneNo+verificationCode</li>
<li>在这块 username仅仅代指登录名, 可以是手机号可以是别的.</li>
<li>这边使用中验证码是从redis中获取的. 获取不到返回失败, 获取到和传递的不一致也算失败.</li>
</ul>
<pre><code class="java">package com.liuzhihang.demo.service;

import com.liuzhihang.demo.bean.UserDetailsImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @author liuzhihang
 */
@Slf4j
@Component(&quot;userDetailServiceImpl&quot;)
public class UserDetailServiceImpl implements UserDetailsService &#123;


    /**
     * 用来验证登录名是否有权限进行登录
     *
     * 可以通过数据库进行校验 也可以通过redis 等等
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException &#123;


        UserDetailsImpl userDetailsImpl = new UserDetailsImpl();
        userDetailsImpl.setUsername(&quot;liuzhihang&quot;);
        userDetailsImpl.setPassword(new BCryptPasswordEncoder().encode(&quot;123456789&quot;));
        return userDetailsImpl;
    &#125;

&#125;
</code></pre>
<h2 id="请求校验过程"><a href="#请求校验过程" class="headerlink" title="请求校验过程"></a>请求校验过程</h2><div class="mermaid">graph TD;
    A(请求接口) --&gt; B(JwtPerTokenFilter#doFilterInternal 验证Header中的token);
    B --&gt; C(DynamicAuthorityService#hasPermission 验证有没有请求url权限);
    C --&gt; D(处理逻辑);
    D --&gt; E(返回结果)</div>



<h3 id="JWTToken拦截器"><a href="#JWTToken拦截器" class="headerlink" title="JWTToken拦截器"></a>JWTToken拦截器</h3><p>主要是拦截请求, 验证Header中的token是否正确</p>
<pre><code class="java">package com.liuzhihang.demo.filter;

import com.liuzhihang.demo.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author liuzhihang
 * @date 2019-06-05 09:09
 */
@Slf4j
@Component
public class JwtPerTokenFilter extends OncePerRequestFilter &#123;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    /**
     * 存放Token的Header Key
     */
    private static final String HEADER_STRING = &quot;token&quot;;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException &#123;

        String token = request.getHeader(HEADER_STRING);
        if (null != token &amp;&amp; !jwtTokenUtil.isTokenExpired(token)) &#123;
            UserDetails userDetails = jwtTokenUtil.getUserDetailsFromToken(token);
            String username = userDetails.getUsername();

            if (username != null &amp;&amp; SecurityContextHolder.getContext().getAuthentication() == null) &#123;

                // 通过 username 查询数据库 获取token 然后和库中token作比较

                if (username.equals(&quot;liuzhihang&quot;)) &#123;

                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                &#125;
            &#125;
        &#125;
        filterChain.doFilter(request, response);
    &#125;

&#125;
</code></pre>
<h3 id="URI动态校验"><a href="#URI动态校验" class="headerlink" title="URI动态校验"></a>URI动态校验</h3><pre><code class="java">package com.liuzhihang.demo.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Set;

/**
 * 动态权限认证
 *
 * @author liuzhihang
 * @date 2019-06-25 15:51
 */
@Slf4j
@Component(value = &quot;dynamicAuthorityService&quot;)
public class DynamicAuthorityService &#123;


    public boolean hasPermission(HttpServletRequest request, Authentication authentication) &#123;

        try &#123;
            Object principal = authentication.getPrincipal();
            if (principal instanceof UserDetails &amp;&amp; authentication instanceof UsernamePasswordAuthenticationToken) &#123;
                // 本次请求的uri
                String uri = request.getRequestURI();

                // 获取当前用户
                UserDetails userDetails = (UserDetails) principal;

                String username = userDetails.getUsername();
                log.info(&quot;本次用户请求认证, username:&#123;&#125;, uri:&#123;&#125;&quot;, username, uri);

                // 从数据库取逻辑
                if (username.equals(&quot;liuzhihang&quot;))&#123;
                    Set&lt;String&gt; set = new HashSet&lt;&gt;();
                    set.add(&quot;/homeInfo&quot;);
                    set.add(&quot;/getAllUser&quot;);
                    set.add(&quot;/editUserInfo&quot;);
                    if (set.contains(uri)) &#123;
                        return true;
                    &#125;
                &#125;

            &#125;
        &#125; catch (Exception e) &#123;
            log.error(&quot;用户请求登录, uri:&#123;&#125; error&quot;, request.getRequestURI(), e);
            return false;
        &#125;
        return false;
    &#125;
&#125;
</code></pre>
<h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>脚本在 <a target="_blank" rel="noopener" href="https://github.com/liuzhihang/jwt-demo/blob/master/src/test/java/ReqTest.http">httpclient脚本</a></p>
<pre><code class="http">POST localhost:8080/login
Content-Type: application/json

&#123;
  &quot;username&quot;: &quot;liuzhihang&quot;,
  &quot;password&quot;: &quot;123456789&quot;
&#125;
### 请求接口脚本

POST localhost:8080/homeInfo
Content-Type: application/json
token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsaXV6aGloYW5nIiwiaWF0IjoxNTY5MDI1NjY4LCJleHAiOjE1Njk2MzA0Njh9.Kot_uLnwtcq-t5o4x3V-xBnpf-mKEi7OV2eAfgMCKLk
###</code></pre>
<p>返回:</p>
<pre><code class="json">&#123;
  &quot;resultCode&quot;: &quot;0000&quot;,
  &quot;resultMsg&quot;: &quot;登录成功&quot;,
  &quot;resultTime&quot;: &quot;20190920191038&quot;,
  &quot;token&quot;: &quot;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsaXV6aGloYW5nIiwiaWF0IjoxNTY4OTc3ODM4LCJleHAiOjE1Njk1ODI2Mzh9.MAS9VkFdCF3agkCgTtc0VzPMFjY42vFyIvAEzkSeAfs&quot;
&#125;</code></pre>
<h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a target="_blank" rel="noopener" href="https://blog.csdn.net/larger5/article/details/81063438">前后端分离 SpringBoot + SpringSecurity + JWT + RBAC 实现用户无状态请求验证</a></p>
</article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta">文章作者: </span><span class="post-copyright-info"><a href="mailto:undefined">liuzhihang</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta">文章链接: </span><span class="post-copyright-info"><a href="https://liuzhihang.com/2019/07/22/springsecurity-jwt-springboot-project.html">https://liuzhihang.com/2019/07/22/springsecurity-jwt-springboot-project.html</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta">版权声明: </span><span class="post-copyright-info">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> 许可协议。转载请注明来自 <a href="https://liuzhihang.com" target="_blank">程序员小航</a>！</span></div></div><div class="tag_share"><div class="post-meta__tag-list"><a class="post-meta__tags" href="/tags/JWT/">JWT</a><a class="post-meta__tags" href="/tags/SpringSecurity/">SpringSecurity</a></div><div class="post_share"><div class="social-share" data-image="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/cyber-security.jpg" data-sites="facebook,twitter,wechat,weibo,qq"></div><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/social-share.js/dist/css/share.min.css" media="print" onload="this.media='all'"><script src="https://cdn.jsdelivr.net/npm/social-share.js/dist/js/social-share.min.js" defer></script></div></div><nav class="pagination-post" id="pagination"><div class="prev-post pull-left"><a href="/2019/08/03/dubbo-bottom-structure-diagram.html"><img class="prev-cover" src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/0730-task.png" onerror="onerror=null;src='https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/92776_n5aac6.jpg'" alt="cover of previous post"><div class="pagination-info"><div class="label">上一篇</div><div class="prev_info">Dubbo底层原理架构图</div></div></a></div><div class="next-post pull-right"><a href="/2019/07/20/gitalk-uses-md5-to-generate-an-id.html"><img class="next-cover" src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/hussam-abd-k1L77eGXZss-unsplash.jpg" onerror="onerror=null;src='https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/92776_n5aac6.jpg'" alt="cover of next post"><div class="pagination-info"><div class="label">下一篇</div><div class="next_info">Gitalk使用MD5生成Id</div></div></a></div></nav><hr/><div id="post-comment"><div class="comment-head"><div class="comment-headline"><i class="fas fa-comments fa-fw"></i><span> 评论</span></div></div><div class="comment-wrap"><div><div id="gitalk-container"></div></div></div></div></div><div class="aside-content" id="aside-content"><div class="card-widget card-info"><div class="is-center"><div class="avatar-img"><img src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/404/avatar.jpg" onerror="this.onerror=null;this.src='/img/friend_404.gif'" alt="avatar"/></div><div class="author-info__name">liuzhihang</div><div class="author-info__description">学，然后知不足；教，然后知困。</div></div><div class="card-info-data"><div class="card-info-data-item is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">145</div></a></div><div class="card-info-data-item is-center"><a href="/tags/"><div class="headline">标签</div><div class="length-num">55</div></a></div><div class="card-info-data-item is-center"><a href="/categories/"><div class="headline">分类</div><div class="length-num">31</div></a></div></div><a class="button--animated" id="card-info-btn" target="_blank" rel="noopener" href="https://github.com/liuzhihang"><i class="fab fa-github"></i><span>Follow Me</span></a><div class="card-info-social-icons is-center"><a class="social-icon" href="https://github.com/liuzhihang" target="_blank" title="Github"><i class="fab fa-github"></i></a><a class="social-icon" href="http://mail.qq.com/cgi-bin/qm_share?t=qm_mailme&amp;email=Vjo-Iyw_Pz43ODElFicneDU5Ow" target="_blank" title="Email"><i class="fas fa-envelope-open"></i></a><a class="social-icon" href="https://weibo.com/onlyhang" target="_blank" title="Weibo"><i class="fab fa-weibo"></i></a><a class="social-icon" href="https://twitter.com/liuzhihangs" target="_blank" title="Twitter"><i class="fab fa-twitter"></i></a><a class="social-icon" href="/atom.xml" target="_blank" title="RSS"><i class="fas fa-rss"></i></a><a class="social-icon" href="https://www.infoq.cn/u/liuzhihang/publish" target="_blank" title="InfoQ"><i class="iconfont iconweibiaoti-1"></i></a><a class="social-icon" href="https://juejin.im/user/1987506650493117" target="_blank" title="掘金"><i class="iconfont iconjuejin-1"></i></a><a class="social-icon" href="https://blog.csdn.net/qq_36535538" target="_blank" title="CSDN"><i class="iconfont iconcsdn"></i></a><a class="social-icon" href="https://www.zhihu.com/people/liuzhihang" target="_blank" title="知乎"><i class="iconfont iconzhihu1"></i></a><a class="social-icon" href="https://leetcode-cn.com/u/liuzhihang" target="_blank" title="LeetCode"><i class="iconfont iconleetcode"></i></a></div></div><div class="card-widget card-announcement"><div class="item-headline"><i class="fas fa-bullhorn card-announcement-animation"></i><span>公告</span></div><div class="announcement_content">🧑‍💻感谢访问本站，若喜欢请收藏 ^_^ <br> <br> <img src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/wechat.jpg"> <br> 个人公众号:『 程序员小航 』</div></div><div class="sticky_layout"><div class="card-widget" id="card-toc"><div class="item-headline"><i class="fas fa-stream"></i><span>目录</span></div><div class="toc-content"><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E8%83%8C%E6%99%AF"><span class="toc-number">1.</span> <span class="toc-text">背景</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E9%85%8D%E7%BD%AE%E8%BF%87%E7%A8%8B"><span class="toc-number">2.</span> <span class="toc-text">配置过程</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%B7%BB%E5%8A%A0%E4%BE%9D%E8%B5%96"><span class="toc-number">2.1.</span> <span class="toc-text">添加依赖</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%9F%BA%E7%A1%80%E5%87%86%E5%A4%87%E5%AF%B9%E8%B1%A1"><span class="toc-number">2.2.</span> <span class="toc-text">基础准备对象</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E5%9F%BA%E7%A1%80%E4%BD%BF%E7%94%A8dto"><span class="toc-number">2.2.1.</span> <span class="toc-text">基础使用dto</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%94%A8%E4%BA%8E%E9%AA%8C%E8%AF%81%E7%9A%84%E7%94%A8%E6%88%B7"><span class="toc-number">2.2.2.</span> <span class="toc-text">用于验证的用户</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%94%A8%E6%88%B7%E6%9C%AA%E7%99%BB%E5%BD%95handle"><span class="toc-number">2.2.3.</span> <span class="toc-text">用户未登录handle</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%94%A8%E6%88%B7%E7%99%BB%E5%BD%95%E9%AA%8C%E8%AF%81%E5%A4%B1%E8%B4%A5handle"><span class="toc-number">2.2.4.</span> <span class="toc-text">用户登录验证失败handle</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%94%A8%E6%88%B7%E6%97%A0%E6%9D%83%E8%AE%BF%E9%97%AEhandle"><span class="toc-number">2.2.5.</span> <span class="toc-text">用户无权访问handle</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%94%A8%E6%88%B7%E7%99%BB%E5%BD%95%E6%88%90%E5%8A%9Fhandle"><span class="toc-number">2.2.6.</span> <span class="toc-text">用户登录成功handle</span></a></li></ol></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#JwtTokenUtil"><span class="toc-number">3.</span> <span class="toc-text">JwtTokenUtil</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#WebSecurityConfig-%E6%A0%B8%E5%BF%83%E9%85%8D%E7%BD%AE"><span class="toc-number">4.</span> <span class="toc-text">WebSecurityConfig 核心配置</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E7%99%BB%E5%BD%95%E6%A0%A1%E9%AA%8C%E8%BF%87%E7%A8%8B"><span class="toc-number">5.</span> <span class="toc-text">登录校验过程</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8B%A6%E6%88%AA%E5%99%A8%E8%A7%A3%E6%9E%90-json-%E6%8A%A5%E6%96%87"><span class="toc-number">5.1.</span> <span class="toc-text">自定义拦截器解析 json 报文</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%94%A8%E6%88%B7%E8%AE%A4%E8%AF%81%E6%A8%A1%E5%9D%97"><span class="toc-number">5.2.</span> <span class="toc-text">用户认证模块</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E8%AF%B7%E6%B1%82%E6%A0%A1%E9%AA%8C%E8%BF%87%E7%A8%8B"><span class="toc-number">6.</span> <span class="toc-text">请求校验过程</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#JWTToken%E6%8B%A6%E6%88%AA%E5%99%A8"><span class="toc-number">6.1.</span> <span class="toc-text">JWTToken拦截器</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#URI%E5%8A%A8%E6%80%81%E6%A0%A1%E9%AA%8C"><span class="toc-number">6.2.</span> <span class="toc-text">URI动态校验</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%B5%8B%E8%AF%95"><span class="toc-number">7.</span> <span class="toc-text">测试</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%8F%82%E8%80%83"><span class="toc-number">8.</span> <span class="toc-text">参考</span></a></li></ol></div></div><div class="card-widget card-recent-post"><div class="item-headline"><i class="fas fa-history"></i><span>最新文章</span></div><div class="aside-list"><div class="aside-list-item"><a class="thumbnail" href="/2021/09/04/the_converter_converts_front_end_parameters_to_enumerations.html" title="使用 SpringBoot 转换器将前端参数转换为枚举"><img src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/article/uztio4-T5n5Wm.jpg" onerror="this.onerror=null;this.src='https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/92776_n5aac6.jpg'" alt="使用 SpringBoot 转换器将前端参数转换为枚举"/></a><div class="content"><a class="title" href="/2021/09/04/the_converter_converts_front_end_parameters_to_enumerations.html" title="使用 SpringBoot 转换器将前端参数转换为枚举">使用 SpringBoot 转换器将前端参数转换为枚举</a><time datetime="2021-09-03T23:00:00.000Z" title="发表于 2021-09-04 07:00:00">2021-09-04</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/2021/08/22/guide_to_mapper_overloading_crater_in_mybatis_plus.html" title="MyBatis-Plus 中 Mapper 重载踩坑指南"><img src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/article/ze1d2z-wXDWp0.jpg" onerror="this.onerror=null;this.src='https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/92776_n5aac6.jpg'" alt="MyBatis-Plus 中 Mapper 重载踩坑指南"/></a><div class="content"><a class="title" href="/2021/08/22/guide_to_mapper_overloading_crater_in_mybatis_plus.html" title="MyBatis-Plus 中 Mapper 重载踩坑指南">MyBatis-Plus 中 Mapper 重载踩坑指南</a><time datetime="2021-08-21T23:00:00.000Z" title="发表于 2021-08-22 07:00:00">2021-08-22</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/2021/07/25/tips_for_locking_and_optimizing_concurrent_scenes.html" title="并发场景加锁优化小技巧"><img src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/article/MODJ8j-9ObHeB.jpg" onerror="this.onerror=null;this.src='https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/92776_n5aac6.jpg'" alt="并发场景加锁优化小技巧"/></a><div class="content"><a class="title" href="/2021/07/25/tips_for_locking_and_optimizing_concurrent_scenes.html" title="并发场景加锁优化小技巧">并发场景加锁优化小技巧</a><time datetime="2021-07-24T23:00:00.000Z" title="发表于 2021-07-25 07:00:00">2021-07-25</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/2021/07/16/curator-5.html" title="ZooKeeper 分布式锁 Curator 源码 05：分布式读写锁和联锁"><img src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/article/EO0tci-8Ju4bN.png" onerror="this.onerror=null;this.src='https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/92776_n5aac6.jpg'" alt="ZooKeeper 分布式锁 Curator 源码 05：分布式读写锁和联锁"/></a><div class="content"><a class="title" href="/2021/07/16/curator-5.html" title="ZooKeeper 分布式锁 Curator 源码 05：分布式读写锁和联锁">ZooKeeper 分布式锁 Curator 源码 05：分布式读写锁和联锁</a><time datetime="2021-07-16T13:30:30.000Z" title="发表于 2021-07-16 21:30:30">2021-07-16</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/2021/07/15/curator-4.html" title="ZooKeeper 分布式锁 Curator 源码 04：分布式信号量和互斥锁"><img src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/article/EO0tci-8Ju4bN.png" onerror="this.onerror=null;this.src='https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/feature/92776_n5aac6.jpg'" alt="ZooKeeper 分布式锁 Curator 源码 04：分布式信号量和互斥锁"/></a><div class="content"><a class="title" href="/2021/07/15/curator-4.html" title="ZooKeeper 分布式锁 Curator 源码 04：分布式信号量和互斥锁">ZooKeeper 分布式锁 Curator 源码 04：分布式信号量和互斥锁</a><time datetime="2021-07-15T13:30:30.000Z" title="发表于 2021-07-15 21:30:30">2021-07-15</time></div></div></div></div></div></div></main><footer id="footer"><div id="footer-wrap"><div class="copyright">&copy;2017 - 2021 By liuzhihang</div><div class="framework-info"><span>框架 </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo</a><span class="footer-separator">|</span><span>主题 </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a></div><div class="footer_custom_text"><a target="_blank" rel="noopener" href="http://www.beian.miit.gov.cn/"><img class="icp-icon" src= "https://cdn.jsdelivr.net/gh/liuzhihang/oss/pic/loading.gif" data-lazy-src="/resources/image/icp.png"><span>备案号：京ICP备20000888号</span></a></div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="readmode" type="button" title="阅读模式"><i class="fas fa-book-open"></i></button><button id="translateLink" type="button" title="简繁转换">繁</button><button id="darkmode" type="button" title="浅色和深色模式转换"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="单栏和双栏切换"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside_config" type="button" title="设置"><i class="fas fa-cog fa-spin"></i></button><button class="close" id="mobile-toc-button" type="button" title="目录"><i class="fas fa-list-ul"></i></button><a id="to_comment" href="#post-comment" title="直达评论"><i class="fas fa-comments"></i></a><button id="go-up" type="button" title="回到顶部"><i class="fas fa-arrow-up"></i></button></div></div><div id="local-search"><div class="search-dialog"><div class="search-dialog__title" id="local-search-title">本地搜索</div><div id="local-input-panel"><div id="local-search-input"><div class="local-search-box"><input class="local-search-box--input" placeholder="搜索文章" type="text"/></div></div></div><hr/><div id="local-search-results"></div><span class="search-close-button"><i class="fas fa-times"></i></span></div><div id="search-mask"></div></div><div><script src="/js/utils.js"></script><script src="/js/main.js"></script><script src="/js/tw_cn.js"></script><script src="https://cdn.jsdelivr.net/npm/instant.page/instantpage.min.js" type="module"></script><script src="https://cdn.jsdelivr.net/npm/vanilla-lazyload/dist/lazyload.iife.min.js"></script><script src="/js/search/local-search.js"></script><div class="js-pjax"><script>if (document.getElementsByClassName('mermaid').length) {
  if (window.mermaidJsLoad) mermaid.init()
  else {
    getScript('https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js').then(() => {
      window.mermaidJsLoad = true
      mermaid.initialize({
        theme: 'default',
      })
      false && mermaid.init()
    })
  }
}</script><script>function addGitalkSource () {
  const ele = document.createElement('link')
  ele.rel = 'stylesheet'
  ele.href= 'https://cdn.jsdelivr.net/npm/gitalk/dist/gitalk.min.css'
  document.getElementsByTagName('head')[0].appendChild(ele)
}

function loadGitalk () {
  function initGitalk () {
    var gitalk = new Gitalk(Object.assign({
      clientID: 'e48f300349e2ac2d03bd',
      clientSecret: '001c669e6269dfd9d7d9fef029cc71fd00ea69e3',
      repo: 'comments',
      owner: 'liuzhihang',
      admin: ['liuzhihang'],
      id: '3f561d4fd36917827d6112122864da84',
      language: 'en',
      perPage: 10,
      distractionFreeMode: false,
      pagerDirection: 'last',
      createIssueManually: false,
      updateCountCallback: commentCount
    },null))

    gitalk.render('gitalk-container')
  }

  if (typeof Gitalk === 'function') initGitalk()
  else {
    addGitalkSource()
    getScript('https://cdn.jsdelivr.net/npm/gitalk@latest/dist/gitalk.min.js').then(initGitalk)
  }
}

function commentCount(n){
  let isCommentCount = document.querySelector('#post-meta .gitalk-comment-count')
  if (isCommentCount) {
    isCommentCount.innerHTML= n
  }
}

if ('Gitalk' === 'Gitalk' || !false) {
  if (false) btf.loadComment(document.getElementById('gitalk-container'), loadGitalk)
  else loadGitalk()
} else {
  function loadOtherComment () {
    loadGitalk()
  }
}</script></div><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script></div></body></html>